介绍
原型模式:用原型实例指定创建对象的种类,并通过复制这些原型创建新的对象。
原型模式是一个创建型的模式。该模式应该有一个样板实例,用户从这个样板对象中复制出一个内部属性一致的对象,这个过程也就是我们俗称的“克隆”。被复制的实例就是我们所称的“原型”,这个原型是可定制的。原型模式多用于创建复杂的或者构造耗时的实例,因为这种情况下,复制一个已经存在的实例可使程序运行更高效。
使用场景
- 类初始化需要消耗非常多的资源,这个资源包括数据、硬件资源等,通过原型复制避免这些消耗。
- 通过 new 产生一个对象需要非常繁琐的数据准备或访问权限,这时可以使用原型模式。
- 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式复制多个对象供调用者使用,即保护性拷贝。
需要注意的是,通过实现 Cloneable 接口的原型模式在调用 clone 函数构造实例时并不一定比通过 new 操作速度快,只有当通过 new 构造对象较为耗时或者成本高时,通过 clone 方法才能够获得效率上的提升。因此,在使用 Cloneable 接口时需要考虑构建对象的成本以及做一些效率上的测试。当然,实现原型模式不一定非要实现 Cloneable 接口,也有其他的实现方式,见后文。
UML 类图
- Client:客户端用户。
- Prototype:抽象类或者接口,声明具备 clone 能力。
- ConcretePrototype:具体的原型类。
示例:文档拷贝
在这个例子中,首先创建了一个文档对象,即 WordDocument,这个文档中含有文字和图片。用户经过了长时间的编辑后,打算对该文档做进一步的编辑。但是,这个编辑后的文档是否会被采用还不确定。因此,为了安全起见,用户需要将当前文档拷贝一份,然后再在文档副本上进行修改。如此,这个原始文档就是我们上述所说的样板实例,也就是将要被“克隆”的对象,我们称为“原型”。
通过 WordDocument 类模拟了 Word 文档中的基本元素,即文字和图片。WordDocument 在该原型模式示例中扮演的角色为 ConcretePrototype,而 Cloneable 的角色则为 Prototype。WordDocument 中的 clone 方法用以实现对象克隆。注意,这个方法并不是 Cloneable 接口的,而是 Object 中的方法。Cloneable 也是一个标识接口,它表明这个类的对象是可拷贝的。如果没有实现 Cloneable 接口却调用了 clone() 函数将抛出异常。在这个示例中,我们通过实现 Cloneable 接口和覆写 clone 方法实现原型模式。
下面看看 Client 端的调用。
输出结果如下所示。
从上面可以看到,doc2 是通过 originDoc.clone() 创建的,并且 doc2 第一次输出的时候和 originDoc 输出是一样的,即 doc2 是 originDoc 的一份拷贝,它们的内容是一样的,而 doc2 修改了文本内容以后并不影响 originDoc 的文本内容,这就保证了 originDoc 的安全性。还需要注意的是,通过 clone 拷贝对象时并不会执行构造函数。
但是,originDoc 的图片列表内容(images)被更改了,这是为什么呢?因为上述示例只是一个浅拷贝。
浅拷贝和深拷贝
- 浅拷贝又叫影子拷贝,上面我们在拷贝文档时并没有把原文档中的字段都重新构造了一遍,而只是拷贝了引用,也就是副文档的字段引用原始文档的字段,这样的话修改副文档中的内容就会连原始文档也改掉了,这就是浅拷贝。
- 深拷贝就是在浅拷贝的基础上,对于引用类型的字段也要采用拷贝的形式,比如上面的 images,而像 String、int 这些基本数据类型则没关系
所以在运用原型模式时建议大家还是用深拷贝。下面我们把上面的浅拷贝改成深拷贝,clone 方法修改如下。
ANDROID 源码中的原型模式
在 Android 中,Intent 可能是我们最早接触的几个类型之一,它用于跳转 Activity、启动服务、发布广播等功能,它是 Android 系统各组件之间的纽带,也是组件之间传递数据的载体,正式 Intent 的存在才使得 Android 各个组件之间的耦合性很低,Android 的组件才如此灵活。
下面以 Intent 来分析源码中的原型模式,首先看如下示例。
通过 shareIntent.clone 方法拷贝了一个对象 Intent,然后执行 startActivity(intent),随即就进入了短信页面,号码为 0800000123,文本内容为 The SMS text,即这些内容都与 shareIntent 一致。
我们看看 Intent 的 clone() 方法是如何实现的。
clone 方法并没有调用 super.clone() 来实现对象拷贝,而是调用了 new Intent(this)。在上文中我们提到过,使用 clone 和 new 需要根据构造对象的成本来决定。如果对象的构造成本比较高或者构造较为麻烦,那么使用 clone() 函数效率较高,否则可以使用 new 的形式。
原型模式实战
在开发中,我们有时候会满足一些需求,就是有的对象的内容只允许客户端程序读取,而不允许修改,比如用户登录信息。我们通常会用 LoginSession 保存用户的登录信息,这些用户信息可能在 APP 的其他模块被用来做登录校验、用户个人信息显示等。但是,这些信息在客户端程序是不允许修改的。此时,就需要使用原型模式来进行保护性拷贝。表现形式如下:
这就使得在任何地方调用 getLoggedUser 函数获取到的用户对象都是一个拷贝对象,即使客户端代码一不小心修改了这个拷贝对象,也不会影响最初的已登录用户对象,对已登录用户信息的修改只能通过 setLoggedUser 这个方法,而只有与 LoginSession 在同一个包下的类才能访问这个包级私有方法。